1   package org.apache.solr.handler;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one or more
5    * contributor license agreements.  See the NOTICE file distributed with
6    * this work for additional information regarding copyright ownership.
7    * The ASF licenses this file to You under the Apache License, Version 2.0
8    * (the "License"); you may not use this file except in compliance with
9    * the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  
21  import java.io.IOException;
22  import java.lang.invoke.MethodHandles;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.HashSet;
29  import java.util.LinkedHashMap;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.concurrent.Callable;
35  import java.util.concurrent.ExecutionException;
36  import java.util.concurrent.ExecutorService;
37  import java.util.concurrent.Future;
38  import java.util.concurrent.TimeUnit;
39  import java.util.concurrent.locks.Lock;
40  import java.util.concurrent.locks.ReentrantLock;
41  
42  import com.google.common.collect.ImmutableSet;
43  import org.apache.solr.client.solrj.SolrClient;
44  import org.apache.solr.client.solrj.SolrRequest;
45  import org.apache.solr.client.solrj.SolrResponse;
46  import org.apache.solr.client.solrj.impl.HttpSolrClient;
47  import org.apache.solr.cloud.ZkController;
48  import org.apache.solr.cloud.ZkSolrResourceLoader;
49  import org.apache.solr.common.SolrException;
50  import org.apache.solr.common.cloud.ClusterState;
51  import org.apache.solr.common.cloud.Replica;
52  import org.apache.solr.common.cloud.Slice;
53  import org.apache.solr.common.cloud.ZkNodeProps;
54  import org.apache.solr.common.params.CommonParams;
55  import org.apache.solr.common.params.MapSolrParams;
56  import org.apache.solr.common.params.ModifiableSolrParams;
57  import org.apache.solr.common.params.SolrParams;
58  import org.apache.solr.common.util.ContentStream;
59  import org.apache.solr.common.util.ExecutorUtil;
60  import org.apache.solr.common.util.NamedList;
61  import org.apache.solr.common.util.StrUtils;
62  import org.apache.solr.common.util.Utils;
63  import org.apache.solr.core.ConfigOverlay;
64  import org.apache.solr.core.ImplicitPlugins;
65  import org.apache.solr.core.PluginInfo;
66  import org.apache.solr.core.RequestParams;
67  import org.apache.solr.core.SolrConfig;
68  import org.apache.solr.core.SolrCore;
69  import org.apache.solr.core.SolrResourceLoader;
70  import org.apache.solr.request.SolrQueryRequest;
71  import org.apache.solr.request.SolrRequestHandler;
72  import org.apache.solr.response.SolrQueryResponse;
73  import org.apache.solr.schema.SchemaManager;
74  import org.apache.solr.util.CommandOperation;
75  import org.apache.solr.util.DefaultSolrThreadFactory;
76  import org.apache.solr.util.RTimer;
77  import org.slf4j.Logger;
78  import org.slf4j.LoggerFactory;
79  
80  import static java.util.Collections.singletonList;
81  import static org.apache.solr.common.util.Utils.makeMap;
82  import static org.apache.solr.common.params.CoreAdminParams.NAME;
83  import static org.apache.solr.common.util.StrUtils.formatString;
84  import static org.apache.solr.core.ConfigOverlay.NOT_EDITABLE;
85  import static org.apache.solr.core.ConfigOverlay.ZNODEVER;
86  import static org.apache.solr.core.ConfigSetProperties.IMMUTABLE_CONFIGSET_ARG;
87  import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_CLASS;
88  import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME;
89  import static org.apache.solr.core.SolrConfig.PluginOpts.REQUIRE_NAME_IN_OVERLAY;
90  import static org.apache.solr.schema.FieldType.CLASS_NAME;
91  
92  public class SolrConfigHandler extends RequestHandlerBase {
93    private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
94    public static final String CONFIGSET_EDITING_DISABLED_ARG = "disable.configEdit";
95    public static final boolean configEditing_disabled = Boolean.getBoolean(CONFIGSET_EDITING_DISABLED_ARG);
96    private static final Map<String, SolrConfig.SolrPluginInfo> namedPlugins;
97    private Lock reloadLock = new ReentrantLock(true);
98    private boolean isImmutableConfigSet = false;
99  
100   static {
101     Map<String, SolrConfig.SolrPluginInfo> map = new HashMap<>();
102     for (SolrConfig.SolrPluginInfo plugin : SolrConfig.plugins) {
103       if (plugin.options.contains(REQUIRE_NAME) || plugin.options.contains(REQUIRE_NAME_IN_OVERLAY)) {
104         map.put(plugin.getCleanTag().toLowerCase(Locale.ROOT), plugin);
105       }
106     }
107     namedPlugins = Collections.unmodifiableMap(map);
108   }
109 
110   @Override
111   public void init(NamedList args) {
112     super.init(args);
113     Object immutable = args.get(IMMUTABLE_CONFIGSET_ARG);
114     isImmutableConfigSet = immutable  != null ? Boolean.parseBoolean(immutable.toString()) : false;
115   }
116 
117   @Override
118   public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
119 
120     setWt(req, CommonParams.JSON);
121     String httpMethod = (String) req.getContext().get("httpMethod");
122     Command command = new Command(req, rsp, httpMethod);
123     if ("POST".equals(httpMethod)) {
124       if (configEditing_disabled || isImmutableConfigSet) {
125         final String reason = configEditing_disabled ? "due to " + CONFIGSET_EDITING_DISABLED_ARG : "because ConfigSet is immutable";
126         throw new SolrException(SolrException.ErrorCode.FORBIDDEN, " solrconfig editing is not enabled " + reason);
127       }
128       try {
129         command.handlePOST();
130       } finally {
131         RequestHandlerUtils.addExperimentalFormatWarning(rsp);
132       }
133     } else {
134       command.handleGET();
135     }
136   }
137 
138 
139   private class Command {
140     private final SolrQueryRequest req;
141     private final SolrQueryResponse resp;
142     private final String method;
143     private String path;
144     List<String> parts;
145 
146     private Command(SolrQueryRequest req, SolrQueryResponse resp, String httpMethod) {
147       this.req = req;
148       this.resp = resp;
149       this.method = httpMethod;
150       path = (String) req.getContext().get("path");
151       if (path == null) path = getDefaultPath();
152       parts = StrUtils.splitSmart(path, '/');
153       if (parts.get(0).isEmpty()) parts.remove(0);
154     }
155 
156     private String getDefaultPath() {
157       return "/config";
158     }
159 
160     private void handleGET() {
161       if (parts.size() == 1) {
162         //this is the whole config. sent out the whole payload
163         resp.add("config", getConfigDetails());
164       } else {
165         if (ConfigOverlay.NAME.equals(parts.get(1))) {
166           resp.add(ConfigOverlay.NAME, req.getCore().getSolrConfig().getOverlay().toMap());
167         } else if (RequestParams.NAME.equals(parts.get(1))) {
168           if (parts.size() == 3) {
169             RequestParams params = req.getCore().getSolrConfig().getRequestParams();
170             MapSolrParams p = params.getParams(parts.get(2));
171             Map m = new LinkedHashMap<>();
172             m.put(ZNODEVER, params.getZnodeVersion());
173             if (p != null) {
174               m.put(RequestParams.NAME, makeMap(parts.get(2), p.getMap()));
175             }
176             resp.add(SolrQueryResponse.NAME, m);
177           } else {
178             resp.add(SolrQueryResponse.NAME, req.getCore().getSolrConfig().getRequestParams().toMap());
179           }
180 
181         } else {
182           if (ZNODEVER.equals(parts.get(1))) {
183             resp.add(ZNODEVER, Utils.makeMap(
184                 ConfigOverlay.NAME, req.getCore().getSolrConfig().getOverlay().getZnodeVersion(),
185                 RequestParams.NAME, req.getCore().getSolrConfig().getRequestParams().getZnodeVersion()));
186             boolean checkStale = false;
187             int expectedVersion = req.getParams().getInt(ConfigOverlay.NAME, -1);
188             int actualVersion = req.getCore().getSolrConfig().getOverlay().getZnodeVersion();
189             if (expectedVersion > actualVersion) {
190               log.info("expecting overlay version {} but my version is {}", expectedVersion, actualVersion);
191               checkStale = true;
192             } else if (expectedVersion != -1) {
193               log.info("I already have the expected version {} of config", expectedVersion);
194             }
195             expectedVersion = req.getParams().getInt(RequestParams.NAME, -1);
196             actualVersion = req.getCore().getSolrConfig().getRequestParams().getZnodeVersion();
197             if (expectedVersion > actualVersion) {
198               log.info("expecting params version {} but my version is {}", expectedVersion, actualVersion);
199               checkStale = true;
200             } else if (expectedVersion != -1) {
201               log.info("I already have the expected version {} of params", expectedVersion);
202             }
203             if (checkStale && req.getCore().getResourceLoader() instanceof ZkSolrResourceLoader) {
204               new Thread(SolrConfigHandler.class.getSimpleName() + "-refreshconf") {
205                 @Override
206                 public void run() {
207                   if (!reloadLock.tryLock()) {
208                     log.info("Another reload is in progress . Not doing anything");
209                     return;
210                   }
211                   try {
212                     log.info("Trying to update my configs");
213                     SolrCore.getConfListener(req.getCore(), (ZkSolrResourceLoader) req.getCore().getResourceLoader()).run();
214                   } catch (Exception e) {
215                     log.error("Unable to refresh conf ", e);
216                   } finally {
217                     reloadLock.unlock();
218                   }
219                 }
220               }.start();
221             } else {
222               log.info("checkStale {} , resourceloader {}", checkStale, req.getCore().getResourceLoader().getClass().getName());
223             }
224 
225           } else {
226             Map<String, Object> m = getConfigDetails();
227             resp.add("config", makeMap(parts.get(1), m.get(parts.get(1))));
228           }
229         }
230       }
231     }
232 
233     private Map<String, Object> getConfigDetails() {
234       Map<String, Object> map = req.getCore().getSolrConfig().toMap();
235       Map reqHandlers = (Map) map.get(SolrRequestHandler.TYPE);
236       if (reqHandlers == null) map.put(SolrRequestHandler.TYPE, reqHandlers = new LinkedHashMap<>());
237       List<PluginInfo> plugins = ImplicitPlugins.getHandlers(req.getCore());
238       for (PluginInfo plugin : plugins) {
239         if (SolrRequestHandler.TYPE.equals(plugin.type)) {
240           if (!reqHandlers.containsKey(plugin.name)) {
241             reqHandlers.put(plugin.name, plugin.toMap());
242           }
243         }
244       }
245       return map;
246     }
247 
248 
249     private void handlePOST() throws IOException {
250       List<CommandOperation> ops = CommandOperation.readCommands(req.getContentStreams(), resp);
251       if (ops == null) return;
252       try {
253         for (; ; ) {
254           ArrayList<CommandOperation> opsCopy = new ArrayList<>(ops.size());
255           for (CommandOperation op : ops) opsCopy.add(op.getCopy());
256           try {
257             if (parts.size() > 1 && RequestParams.NAME.equals(parts.get(1))) {
258               RequestParams params = RequestParams.getFreshRequestParams(req.getCore().getResourceLoader(), req.getCore().getSolrConfig().getRequestParams());
259               handleParams(opsCopy, params);
260             } else {
261               ConfigOverlay overlay = SolrConfig.getConfigOverlay(req.getCore().getResourceLoader());
262               handleCommands(opsCopy, overlay);
263             }
264             break;//succeeded . so no need to go over the loop again
265           } catch (ZkController.ResourceModifiedInZkException e) {
266             //retry
267             log.info("Race condition, the node is modified in ZK by someone else " + e.getMessage());
268           }
269         }
270       } catch (Exception e) {
271         resp.setException(e);
272         resp.add(CommandOperation.ERR_MSGS, singletonList(SchemaManager.getErrorStr(e)));
273       }
274 
275     }
276 
277 
278     private void handleParams(ArrayList<CommandOperation> ops, RequestParams params) {
279       for (CommandOperation op : ops) {
280         switch (op.name) {
281           case SET:
282           case UPDATE: {
283             Map<String, Object> map = op.getDataMap();
284             if (op.hasError()) break;
285 
286             for (Map.Entry<String, Object> entry : map.entrySet()) {
287 
288               Map val = null;
289               String key = entry.getKey();
290               if (key == null || key.trim().isEmpty()) {
291                 op.addError("null key ");
292                 continue;
293               }
294               key = key.trim();
295               String err = validateName(key);
296               if (err != null) {
297                 op.addError(err);
298                 continue;
299               }
300 
301               try {
302                 val = (Map) entry.getValue();
303               } catch (Exception e1) {
304                 op.addError("invalid params for key : " + key);
305                 continue;
306               }
307 
308               if (val.containsKey("")) {
309                 op.addError("Empty keys are not allowed in params");
310                 continue;
311               }
312 
313               MapSolrParams old = params.getParams(key);
314               if (op.name.equals(UPDATE)) {
315                 LinkedHashMap m = new LinkedHashMap(old.getMap());
316                 m.putAll(val);
317                 val = m;
318               }
319               params = params.setParams(key, val);
320 
321             }
322             break;
323 
324           }
325           case "delete": {
326             List<String> name = op.getStrs(CommandOperation.ROOT_OBJ);
327             if (op.hasError()) break;
328             for (String s : name) {
329               if (params.getParams(s) == null) {
330                 op.addError(formatString("can't delete . No such params ''{0}'' exist", s));
331               }
332               params = params.setParams(s, null);
333             }
334           }
335         }
336       }
337 
338 
339       List errs = CommandOperation.captureErrors(ops);
340       if (!errs.isEmpty()) {
341         resp.add(CommandOperation.ERR_MSGS, errs);
342         return;
343       }
344 
345       SolrResourceLoader loader = req.getCore().getResourceLoader();
346       if (loader instanceof ZkSolrResourceLoader) {
347         ZkSolrResourceLoader zkLoader = (ZkSolrResourceLoader) loader;
348         if (ops.isEmpty()) {
349           ZkController.touchConfDir(zkLoader);
350         } else {
351           log.info("persisting params version : {}", params.toMap());
352           int latestVersion = ZkController.persistConfigResourceToZooKeeper(zkLoader,
353               params.getZnodeVersion(),
354               RequestParams.RESOURCE,
355               params.toByteArray(), true);
356           waitForAllReplicasState(req.getCore().getCoreDescriptor().getCloudDescriptor().getCollectionName(),
357               req.getCore().getCoreDescriptor().getCoreContainer().getZkController(),
358               RequestParams.NAME,
359               latestVersion, 30);
360         }
361 
362       } else {
363         SolrResourceLoader.persistConfLocally(loader, RequestParams.RESOURCE, params.toByteArray());
364         req.getCore().getSolrConfig().refreshRequestParams();
365       }
366 
367     }
368 
369     private void handleCommands(List<CommandOperation> ops, ConfigOverlay overlay) throws IOException {
370       for (CommandOperation op : ops) {
371         switch (op.name) {
372           case SET_PROPERTY:
373             overlay = applySetProp(op, overlay);
374             break;
375           case UNSET_PROPERTY:
376             overlay = applyUnset(op, overlay);
377             break;
378           case SET_USER_PROPERTY:
379             overlay = applySetUserProp(op, overlay);
380             break;
381           case UNSET_USER_PROPERTY:
382             overlay = applyUnsetUserProp(op, overlay);
383             break;
384           default: {
385             List<String> pcs = StrUtils.splitSmart(op.name.toLowerCase(Locale.ROOT), '-');
386             if (pcs.size() != 2) {
387               op.addError(formatString("Unknown operation ''{0}'' ", op.name));
388             } else {
389               String prefix = pcs.get(0);
390               String name = pcs.get(1);
391               if (cmdPrefixes.contains(prefix) && namedPlugins.containsKey(name)) {
392                 SolrConfig.SolrPluginInfo info = namedPlugins.get(name);
393                 if ("delete".equals(prefix)) {
394                   overlay = deleteNamedComponent(op, overlay, info.getCleanTag());
395                 } else {
396                   overlay = updateNamedPlugin(info, op, overlay, prefix.equals("create") || prefix.equals("add"));
397                 }
398               } else {
399                 op.unknownOperation();
400               }
401             }
402           }
403         }
404       }
405       List errs = CommandOperation.captureErrors(ops);
406       if (!errs.isEmpty()) {
407         log.info("Failed to run commands. errors are {}", StrUtils.join(errs, ','));
408         resp.add(CommandOperation.ERR_MSGS, errs);
409         return;
410       }
411 
412       SolrResourceLoader loader = req.getCore().getResourceLoader();
413       if (loader instanceof ZkSolrResourceLoader) {
414         int latestVersion = ZkController.persistConfigResourceToZooKeeper((ZkSolrResourceLoader) loader, overlay.getZnodeVersion(),
415             ConfigOverlay.RESOURCE_NAME, overlay.toByteArray(), true);
416         log.info("Executed config commands successfully and persisted to ZK {}", ops);
417         waitForAllReplicasState(req.getCore().getCoreDescriptor().getCloudDescriptor().getCollectionName(),
418             req.getCore().getCoreDescriptor().getCoreContainer().getZkController(),
419             ConfigOverlay.NAME,
420             latestVersion, 30);
421       } else {
422         SolrResourceLoader.persistConfLocally(loader, ConfigOverlay.RESOURCE_NAME, overlay.toByteArray());
423         req.getCore().getCoreDescriptor().getCoreContainer().reload(req.getCore().getName());
424         log.info("Executed config commands successfully and persited to File System {}", ops);
425       }
426 
427     }
428 
429     private ConfigOverlay deleteNamedComponent(CommandOperation op, ConfigOverlay overlay, String typ) {
430       String name = op.getStr(CommandOperation.ROOT_OBJ);
431       if (op.hasError()) return overlay;
432       if (overlay.getNamedPlugins(typ).containsKey(name)) {
433         return overlay.deleteNamedPlugin(name, typ);
434       } else {
435         op.addError(formatString("NO such {0} ''{1}'' ", typ, name));
436         return overlay;
437       }
438     }
439 
440     private ConfigOverlay updateNamedPlugin(SolrConfig.SolrPluginInfo info, CommandOperation op, ConfigOverlay overlay, boolean isCeate) {
441       String name = op.getStr(NAME);
442       String clz = info.options.contains(REQUIRE_CLASS) ? op.getStr(CLASS_NAME) : op.getStr(CLASS_NAME, null);
443       op.getMap(PluginInfo.DEFAULTS, null);
444       op.getMap(PluginInfo.INVARIANTS, null);
445       op.getMap(PluginInfo.APPENDS, null);
446       if (op.hasError()) return overlay;
447       if (!verifyClass(op, clz, info.clazz)) return overlay;
448       if (pluginExists(info, overlay, name)) {
449         if (isCeate) {
450           op.addError(formatString(" ''{0}'' already exists . Do an ''{1}'' , if you want to change it ", name, "update-" + info.getTagCleanLower()));
451           return overlay;
452         } else {
453           return overlay.addNamedPlugin(op.getDataMap(), info.getCleanTag());
454         }
455       } else {
456         if (isCeate) {
457           return overlay.addNamedPlugin(op.getDataMap(), info.getCleanTag());
458         } else {
459           op.addError(formatString(" ''{0}'' does not exist . Do an ''{1}'' , if you want to create it ", name, "create-" + info.getTagCleanLower()));
460           return overlay;
461         }
462       }
463     }
464 
465     private boolean pluginExists(SolrConfig.SolrPluginInfo info, ConfigOverlay overlay, String name) {
466       List<PluginInfo> l = req.getCore().getSolrConfig().getPluginInfos(info.clazz.getName());
467       for (PluginInfo pluginInfo : l) if(name.equals( pluginInfo.name)) return true;
468       return overlay.getNamedPlugins(info.getCleanTag()).containsKey(name);
469     }
470 
471     private boolean verifyClass(CommandOperation op, String clz, Class expected) {
472       if (clz == null) return true;
473       if (!"true".equals(String.valueOf(op.getStr("runtimeLib", null)))) {
474         //this is not dynamically loaded so we can verify the class right away
475         try {
476           req.getCore().createInitInstance(new PluginInfo(SolrRequestHandler.TYPE, op.getDataMap()), expected, clz, "");
477         } catch (Exception e) {
478           op.addError(e.getMessage());
479           return false;
480         }
481 
482       }
483       return true;
484     }
485 
486     private ConfigOverlay applySetUserProp(CommandOperation op, ConfigOverlay overlay) {
487       Map<String, Object> m = op.getDataMap();
488       if (op.hasError()) return overlay;
489       for (Map.Entry<String, Object> e : m.entrySet()) {
490         String name = e.getKey();
491         Object val = e.getValue();
492         overlay = overlay.setUserProperty(name, val);
493       }
494       return overlay;
495     }
496 
497     private ConfigOverlay applyUnsetUserProp(CommandOperation op, ConfigOverlay overlay) {
498       List<String> name = op.getStrs(CommandOperation.ROOT_OBJ);
499       if (op.hasError()) return overlay;
500       for (String o : name) {
501         if (!overlay.getUserProps().containsKey(o)) {
502           op.addError(formatString("No such property ''{0}''", name));
503         } else {
504           overlay = overlay.unsetUserProperty(o);
505         }
506       }
507       return overlay;
508     }
509 
510 
511     private ConfigOverlay applyUnset(CommandOperation op, ConfigOverlay overlay) {
512       List<String> name = op.getStrs(CommandOperation.ROOT_OBJ);
513       if (op.hasError()) return overlay;
514 
515       for (String o : name) {
516         if (!ConfigOverlay.isEditableProp(o, false, null)) {
517           op.addError(formatString(NOT_EDITABLE, name));
518         } else {
519           overlay = overlay.unsetProperty(o);
520         }
521       }
522       return overlay;
523     }
524 
525     private ConfigOverlay applySetProp(CommandOperation op, ConfigOverlay overlay) {
526       Map<String, Object> m = op.getDataMap();
527       if (op.hasError()) return overlay;
528       for (Map.Entry<String, Object> e : m.entrySet()) {
529         String name = e.getKey();
530         Object val = e.getValue();
531         Class typ = ConfigOverlay.checkEditable(name, false, null);
532         if (typ == null) {
533           op.addError(formatString(NOT_EDITABLE, name));
534           continue;
535         }
536 
537         if (val != null) {
538           if (typ == String.class) val = val.toString();
539           String typeErr = "Property {0} must be of {1} type ";
540           if (typ == Boolean.class) {
541             try {
542               val = Boolean.parseBoolean(val.toString());
543             } catch (Exception exp) {
544               op.addError(formatString(typeErr, name, typ.getSimpleName()));
545               continue;
546             }
547           } else if (typ == Integer.class) {
548             try {
549               val = Integer.parseInt(val.toString());
550             } catch (Exception exp) {
551               op.addError(formatString(typeErr, typ.getSimpleName()));
552               continue;
553             }
554 
555           } else if (typ == Float.class) {
556             try {
557               val = Float.parseFloat(val.toString());
558             } catch (Exception exp) {
559               op.addError(formatString(typeErr, typ.getSimpleName()));
560               continue;
561             }
562 
563           }
564         }
565 
566 
567         overlay = overlay.setProperty(name, val);
568       }
569       return overlay;
570     }
571 
572   }
573 
574   public static String validateName(String s) {
575     for (int i = 0; i < s.length(); i++) {
576       char c = s.charAt(i);
577       if ((c >= 'A' && c <= 'Z') ||
578           (c >= 'a' && c <= 'z') ||
579           (c >= '0' && c <= '9') ||
580           c == '_' ||
581           c == '-' ||
582           c == '.'
583           ) continue;
584       else {
585         return formatString("''{0}'' name should only have chars [a-zA-Z_-.0-9] ", s);
586       }
587     }
588     return null;
589   }
590 
591   public static void setWt(SolrQueryRequest req, String wt) {
592     SolrParams params = req.getParams();
593     if (params.get(CommonParams.WT) != null) return;//wt is set by user
594     Map<String, String> map = new HashMap<>(1);
595     map.put(CommonParams.WT, wt);
596     map.put("indent", "true");
597     req.setParams(SolrParams.wrapDefaults(params, new MapSolrParams(map)));
598   }
599 
600   @Override
601   public SolrRequestHandler getSubHandler(String path) {
602     if (subPaths.contains(path)) return this;
603     if (path.startsWith("/params/")) return this;
604     return null;
605   }
606 
607 
608   private static Set<String> subPaths = new HashSet<>(Arrays.asList("/overlay", "/params", "/updateHandler",
609       "/query", "/jmx", "/requestDispatcher", "/znodeVersion"));
610 
611   static {
612     for (SolrConfig.SolrPluginInfo solrPluginInfo : SolrConfig.plugins)
613       subPaths.add("/" + solrPluginInfo.getCleanTag());
614 
615   }
616 
617   //////////////////////// SolrInfoMBeans methods //////////////////////
618 
619 
620   @Override
621   public String getDescription() {
622     return "Edit solrconfig.xml";
623   }
624 
625 
626   @Override
627   public String getVersion() {
628     return getClass().getPackage().getSpecificationVersion();
629   }
630 
631   @Override
632   public Category getCategory() {
633     return Category.OTHER;
634   }
635 
636 
637   public static final String SET_PROPERTY = "set-property";
638   public static final String UNSET_PROPERTY = "unset-property";
639   public static final String SET_USER_PROPERTY = "set-user-property";
640   public static final String UNSET_USER_PROPERTY = "unset-user-property";
641   public static final String SET = "set";
642   public static final String UPDATE = "update";
643   public static final String CREATE = "create";
644   private static Set<String> cmdPrefixes = ImmutableSet.of(CREATE, UPDATE, "delete", "add");
645 
646   /**
647    * Block up to a specified maximum time until we see agreement on the schema
648    * version in ZooKeeper across all replicas for a collection.
649    */
650   private static void waitForAllReplicasState(String collection,
651                                               ZkController zkController,
652                                               String prop,
653                                               int expectedVersion,
654                                               int maxWaitSecs) {
655     final RTimer timer = new RTimer();
656     // get a list of active replica cores to query for the schema zk version (skipping this core of course)
657     List<PerReplicaCallable> concurrentTasks = new ArrayList<>();
658 
659     for (String coreUrl : getActiveReplicaCoreUrls(zkController, collection)) {
660       PerReplicaCallable e = new PerReplicaCallable(coreUrl, prop, expectedVersion, maxWaitSecs);
661       concurrentTasks.add(e);
662     }
663     if (concurrentTasks.isEmpty()) return; // nothing to wait for ...
664 
665     log.info(formatString("Waiting up to {0} secs for {1} replicas to set the property {2} to be of version {3} for collection {4}",
666         maxWaitSecs, concurrentTasks.size(), prop, expectedVersion, collection));
667 
668     // use an executor service to invoke schema zk version requests in parallel with a max wait time
669     int poolSize = Math.min(concurrentTasks.size(), 10);
670     ExecutorService parallelExecutor =
671         ExecutorUtil.newMDCAwareFixedThreadPool(poolSize, new DefaultSolrThreadFactory("solrHandlerExecutor"));
672     try {
673       List<Future<Boolean>> results =
674           parallelExecutor.invokeAll(concurrentTasks, maxWaitSecs, TimeUnit.SECONDS);
675 
676       // determine whether all replicas have the update
677       List<String> failedList = null; // lazily init'd
678       for (int f = 0; f < results.size(); f++) {
679         Boolean success = false;
680         Future<Boolean> next = results.get(f);
681         if (next.isDone() && !next.isCancelled()) {
682           // looks to have finished, but need to check if it succeeded
683           try {
684             success = next.get();
685           } catch (ExecutionException e) {
686             // shouldn't happen since we checked isCancelled
687           }
688         }
689 
690         if (!success) {
691           String coreUrl = concurrentTasks.get(f).coreUrl;
692           log.warn("Core " + coreUrl + "could not get the expected version " + expectedVersion);
693           if (failedList == null) failedList = new ArrayList<>();
694           failedList.add(coreUrl);
695         }
696       }
697 
698       // if any tasks haven't completed within the specified timeout, it's an error
699       if (failedList != null)
700         throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
701             formatString("{0} out of {1} the property {2} to be of version {3} within {4} seconds! Failed cores: {5}",
702                 failedList.size(), concurrentTasks.size() + 1, prop, expectedVersion, maxWaitSecs, failedList));
703 
704     } catch (InterruptedException ie) {
705       log.warn(formatString(
706           "Core  was interrupted . trying to set the property {1} to version {2} to propagate to {3} replicas for collection {4}",
707           prop, expectedVersion, concurrentTasks.size(), collection));
708       Thread.currentThread().interrupt();
709     } finally {
710       ExecutorUtil.shutdownAndAwaitTermination(parallelExecutor);
711     }
712 
713     log.info("Took {}ms to set the property {} to be of version {} for collection {}",
714         timer.getTime(), prop, expectedVersion, collection);
715   }
716 
717   public static List<String> getActiveReplicaCoreUrls(ZkController zkController,
718                                                       String collection) {
719     List<String> activeReplicaCoreUrls = new ArrayList<>();
720     ClusterState clusterState = zkController.getZkStateReader().getClusterState();
721     Set<String> liveNodes = clusterState.getLiveNodes();
722     Collection<Slice> activeSlices = clusterState.getActiveSlices(collection);
723     if (activeSlices != null && activeSlices.size() > 0) {
724       for (Slice next : activeSlices) {
725         Map<String, Replica> replicasMap = next.getReplicasMap();
726         if (replicasMap != null) {
727           for (Map.Entry<String, Replica> entry : replicasMap.entrySet()) {
728             Replica replica = entry.getValue();
729             if (replica.getState() == Replica.State.ACTIVE && liveNodes.contains(replica.getNodeName())) {
730               activeReplicaCoreUrls.add(replica.getCoreUrl());
731             }
732           }
733         }
734       }
735     }
736     return activeReplicaCoreUrls;
737   }
738 
739   private static class PerReplicaCallable extends SolrRequest implements Callable<Boolean> {
740     String coreUrl;
741     String prop;
742     int expectedZkVersion;
743     Number remoteVersion = null;
744     int maxWait;
745 
746     PerReplicaCallable(String coreUrl, String prop, int expectedZkVersion, int maxWait) {
747       super(METHOD.GET, "/config/" + ZNODEVER);
748       this.coreUrl = coreUrl;
749       this.expectedZkVersion = expectedZkVersion;
750       this.prop = prop;
751       this.maxWait = maxWait;
752     }
753 
754     @Override
755     public SolrParams getParams() {
756       return new ModifiableSolrParams()
757           .set(prop, expectedZkVersion)
758           .set(CommonParams.WT, CommonParams.JAVABIN);
759     }
760 
761     @Override
762     public Boolean call() throws Exception {
763       final RTimer timer = new RTimer();
764       int attempts = 0;
765       try (HttpSolrClient solr = new HttpSolrClient(coreUrl)) {
766         // eventually, this loop will get killed by the ExecutorService's timeout
767         while (true) {
768           try {
769             long timeElapsed = (long) timer.getTime() / 1000;
770             if (timeElapsed >= maxWait) {
771               return false;
772             }
773             log.info("Time elapsed : {} secs, maxWait {}", timeElapsed, maxWait);
774             Thread.sleep(100);
775             NamedList<Object> resp = solr.httpUriRequest(this).future.get();
776             if (resp != null) {
777               Map m = (Map) resp.get(ZNODEVER);
778               if (m != null) {
779                 remoteVersion = (Number) m.get(prop);
780                 if (remoteVersion != null && remoteVersion.intValue() >= expectedZkVersion) break;
781               }
782             }
783 
784             attempts++;
785             log.info(formatString("Could not get expectedVersion {0} from {1} for prop {2}   after {3} attempts", expectedZkVersion, coreUrl, prop, attempts));
786           } catch (Exception e) {
787             if (e instanceof InterruptedException) {
788               break; // stop looping
789             } else {
790               log.warn("Failed to get /schema/zkversion from " + coreUrl + " due to: " + e);
791             }
792           }
793         }
794       }
795       return true;
796     }
797 
798     @Override
799     public Collection<ContentStream> getContentStreams() throws IOException {
800       return null;
801     }
802 
803     @Override
804     protected SolrResponse createResponse(SolrClient client) {
805       return null;
806     }
807   }
808 }